package uk.co.caprica.vlcj.oop.process;

import static uk.co.caprica.vlcj.oop.registry.ObjectName.MEDIA_PLAYER;
import static uk.co.caprica.vlcj.oop.registry.ObjectName.MEDIA_PLAYER_PROCESS;
import static uk.co.caprica.vlcj.oop.registry.ObjectName.RENDEZVOUS;
import gnu.cajo.invoke.Remote;
import gnu.cajo.utils.ItemServer;

import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import uk.co.caprica.vlcj.logger.Logger;
import uk.co.caprica.vlcj.oop.OutOfProcessMediaPlayer;
import uk.co.caprica.vlcj.oop.OutOfProcessMediaPlayerFactory;
import uk.co.caprica.vlcj.oop.registry.RemotingRegistry;

// FIXME not sure the heart-beat is working properly - it seems to shutdown the process, but the process terminates before the final log is written out

/**
 * Encapsulation of a media player process.
 * <p>
 * This class is the main class for the "out-of-process" media player process.
 * <p>
 * The implementation goes like this:
 * <ul>
 *   <li>Create new media player process;</li>
 *   <li>Install a shutdown hook for the process so that it cleans up when the JVM terminates;</li>
 *   <li>Bind the process and media player instances in the remoting service;</li>
 *   <li>Register the out-of-process media player with the parent application process.</li>
 * </ul>
 * Note that <em>any public method</em> on this class is remotely invokable by the parent 
 * application process.
 * 
 * TODO implement a heart-beat to detect if the main application is still alive (listeners will likely start crashing) maybe some sort of executorsservice or something
 */
public class MediaPlayerProcess {

    // FIXME replace with enum
    public static final String RENDEZVOUS_INIT_METHOD = "initialise";
    public static final String RENDEZVOUS_PING_METHOD = "ping";
    
    private static final int HEARTBEAT_PERIOD = 3000; // FIXME how to make configurable? pass in? some config could be returned by the remote initialisation call? but heartbeat should work even if that config is not available! prolly a property file or pass in via args?
    
    /**
     * Default factory initialisation arguments.
     * <p>
     * Sub-classes may totally disregard these arguments and provide their own - for example to
     * enable ffmpeg hardware decoding via "--ffmpeg-hw" if supported by the OS and the GPU.
     * <p>
     * A sub-class has access to these default arguments so new ones could be merged with these if
     * required.
     */
    protected static final String[] DEFAULT_FACTORY_ARGUMENTS = {
        "--no-plugins-cache", 
        "--no-video-title-show", 
        "--no-snapshot-preview", 
        "--quiet", 
        "--quiet-synchro", 
        "--intf", 
        "dummy"
    };

    /**
     * 
     */
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    
    /**
     * Host name for the remote call-back.
     * <p>
     * This is supplied as a command-line argument when creating the process. 
     */
    private final String callbackHost;

    /**
     * Port number for the remote call-back.
     * <p>
     * This is supplied as a command-line argument when creating the process. 
     */
    private final int callbackPort;

    /**
     * Unique process identifier.
     * <p>
     * This is supplied as a command-line argument when creating the process. 
     */
    private final int id;

    /**
     * Native media player factory.
     */
    private final OutOfProcessMediaPlayerFactory mediaPlayerFactory;

    /**
     * Native media player instance.
     */
    private final OutOfProcessMediaPlayer mediaPlayer;

    /**
     * Process entry point.
     * <p>
     * This process will not exit because the remoting infrastructure is running (i.e. non-daemon
     * threads will continue to run).
     * <p>
     * The parent application must supply <em>exactly three</em> arguments when launching this
     * process:
     * <ul>
     *   <li>Name of the host on which the parent application resides;</li>
     *   <li>Number of the port on the parent application host used for remoting;</li>
     *   <li>Unique identifier for this process.</li>
     * </ul>
     * If any of those mandatory arguments are missing or are invalid then the process will
     * fail to launch.
     * 
     * @param args command-line arguments
     */
    public static void main(String[] args) {
        Logger.debug("Created new process");
        // Create a new process instance, passing along the command-line arguments as
        // supplied by the parent process
        final MediaPlayerProcess mediaPlayerProcess = new MediaPlayerProcess(args);
        Logger.debug("mediaPlayerProcess={}", mediaPlayerProcess);
        Logger.debug("Process started");
    }

    /**
     * Create a new media player process.
     * 
     * @param args process arguments
     */
    public MediaPlayerProcess(String[] args) {
        Logger.debug("MediaPlayerProcess(args={})", Arrays.toString(args));
        // Get the call-back details and process identifier from the supplied arguments
        this.callbackHost = args[0];
        this.callbackPort = Integer.parseInt(args[1]);
        this.id = Integer.parseInt(args[2]);
        // Register a shutdown hook to clean up this process when the parent process terminates
        installShutdownHook();
        // Create the native media player resources
        mediaPlayerFactory = onGetMediaPlayerFactory();
        mediaPlayer = mediaPlayerFactory.newOutOfProcessMediaPlayer();
        try {
            // Bind the process and media player in the remoting server
            bindObjects();
            // Register the new out-of-process media player with the parent application process
            registerOutOfProcessMediaPlayer();
        }
        catch(Throwable t) {
            Logger.error("Failed to start process", t);
            // Clean up
            terminate();
            // Fatal exit
            System.exit(1);
        }
        // Start the heart-beat monitor
        startHeartbeatMonitor();
        Logger.debug("Process started successfully.");
    }

    /**
     * Install a shut-down hook to ensure the remoting infrastructure is cleanly terminated when the
     * virtual machine exits.
     * <p>
     * Without this, the remoting server port will not be released immediately when the virtual
     * machine terminates.
     */
    private void installShutdownHook() {
        Logger.debug("installShutdownHook()");
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                Logger.debug("run()");
                Logger.debug("Shutdown remoting server in shutdown hook");
                Remote.shutdown();
                Logger.debug("Remoting server has been shut down");
            }
        });
    }

    /**
     * Template method to create a media player factory.
     * <p>
     * The default implementation will invoke the {@link #onGetMediaPlayerFactoryArgs()} template
     * method.
     * <p>
     * When this component is released via {@link #release()} the factory instance returned by this
     * method will also be released.
     * 
     * @return media player factory
     */
    protected OutOfProcessMediaPlayerFactory onGetMediaPlayerFactory() {
        return new OutOfProcessMediaPlayerFactory(onGetMediaPlayerFactoryArgs());
    }

    /**
     * Template method to obtain the initialisation arguments used to create the media player
     * factory instance.
     * <p>
     * If a sub-class overrides the {@link #onGetMediaPlayerFactory()} template method there is no
     * guarantee that {@link #onGetMediaPlayerFactoryArgs()} will be called.
     * 
     * @return media player factory initialisation arguments
     */
    protected String[] onGetMediaPlayerFactoryArgs() {
        return DEFAULT_FACTORY_ARGUMENTS;
    }
    
    /**
     * Bind the media player process objects in the remoting server.
     * <p>
     * Two objects are bound:
     * <ul>
     *   <li>This process instance;</li>
     *   <li>Media player instance.</li>
     * </ul> 
     */
    private void bindObjects() {
        Logger.debug("bindObjects()");
        try {
            // Start the remoting infrastructure
            Remote.config(null, 0, null, 0);
            // Bind this process instance
            ItemServer.bind(this, MEDIA_PLAYER_PROCESS.objectName());
            // Bind the media player instance
            ItemServer.bind(mediaPlayer, MEDIA_PLAYER.objectName());
            // Report some useful information
            Logger.info("Server host: {}", Remote.getDefaultServerHost());
            Logger.info("Server port: {}", Remote.getDefaultServerPort());
        }
        catch(IOException e) {
            throw new RuntimeException("Failed to bind media player", e);
        }
    }

    /**
     * Register the remote media player with the main application.
     * <p>
     * This will execute a remote call back to a rendezvous object bound in the main application.
     * <p>
     * Without this synchronisation, the main application can not know when the native media player
     * is 'ready'.
     * <p>
     * The actual content of the call-back is to inform the main application of the address of the
     * remote media player object so that it can be controlled by the main application.
     */
    private void registerOutOfProcessMediaPlayer() {
        Logger.debug("registerOutOfProcessMediaPlayer()");
        try {
            // Lookup the rendezvous object in the remoting server - the name of 
            // the object follows a fixed pattern, ending with the unique identifier
            // assigned to this particular process
            Object rendezvousObject = Remote.getItem(getRendezvousLookupName());
            Logger.debug("rendezvousObject={}", rendezvousObject);
            // Invoke the initialisation method on the remote rendezvous object, 
            // suppling the binding address of this process
            Remote.invoke(rendezvousObject, RENDEZVOUS_INIT_METHOD, new Object[] {Remote.getDefaultServerHost(), Remote.getDefaultServerPort()});
            Logger.debug("Process registered with remoting registry");
        }
        catch(Exception e) {
            // The client most likely went away
            throw new RuntimeException("Failed to rendezvous with main process", e);
        }
    }

    /**
     * Start the remote heart-beat monitor.
     * <p>
     * This heart-beat executes in a background thread and periodically pings the
     * main application process to check if it is still alive and responding.
     * <p>
     * If the main application is not responding, this process is terminated.
     */
    private void startHeartbeatMonitor() {
        Logger.debug("startHeartbeatMonitor()");
        executor.submit(new Runnable() {
            @Override
            public void run() {
                for(;;) {
                    try {
                        Thread.sleep(HEARTBEAT_PERIOD);
                    }
                    catch(InterruptedException e) {
                        Logger.warn("Heartbeat monitor interrupted, terminating");
                        return;
                    }
                    try {
                        ping();
                    }
                    catch(Exception e) {
                        Logger.warn("Failed to ping remote rendezvous, terminating", e.getMessage());
                        terminate();
                    }
                }
            }
        });
    }
    
    /**
     * Ping the remote rendezvous object to see if it is still alive and responding.
     * <p>
     * If the remote object is not responding, terminate this process.
     */
    private void ping() throws Exception {
        Logger.trace("ping()");
        Object rendezvousObject = Remote.getItem(getRendezvousLookupName());
        Logger.trace("rendezvousObject", rendezvousObject);
        Remote.invoke(rendezvousObject, RENDEZVOUS_PING_METHOD, null);
    }

    /**
     * Get the name of the remote rendezvous object bound in the parent process.
     * 
     * @return item name
     */
    private String getRendezvousLookupName() {
        StringBuilder sb = new StringBuilder(40);
        sb.append(RemotingRegistry.objectName(callbackHost, callbackPort, RENDEZVOUS.objectName())).append(id);
        String rendezvousLookupName = sb.toString();
        Logger.trace("rendezvousLookupName={}", rendezvousLookupName);
        return rendezvousLookupName;
    }

    /**
     * Terminate this process.
     * <p>
     * An attempt is made to cleanly terminate this process:
     * <ul>
     *   <li>Remote objects are unbound from the remoting server;</li>
     *   <li>The native media player instance is stopped and native resources are released;</li>
     *   <li>The remoting server associated with this process is stopped;</li>
     *   <li>This process is terminated.</li>
     * </ul>
     * 
     * This method is remotely invokable by the parent application process. 
     */
    public final void terminate() {
        Logger.debug("terminate()");
        // Stop the heart-beat monitor
        executor.shutdownNow();
        // Unbind the objects exposed for remoting so that they can no longer be invoked
        ItemServer.unbind(MEDIA_PLAYER_PROCESS.objectName());
        ItemServer.unbind(MEDIA_PLAYER.objectName());
        // Terminate and free the native media player resources
        if(mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.release();
        }
        if(mediaPlayerFactory != null) {
            mediaPlayerFactory.release();
        }
        // Terminate the remoting server associated with this process
        Logger.debug("Shut down Remote");
        Remote.shutdown();
        Logger.debug("Done shut down remote");

        
        
        // FIXME what follows needs to be fixed
        
        // If you add a listener at the client, this won't exit, so... need a clean way to do this
        // try {
        // Thread.sleep(1000);
        // }
        // catch(InterruptedException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }

        // This is not ideal, but I think it will remove most exceptions from the client

        // Allow the remote client call to complete normally by scheduling the
        // process to terminate in a short while
        Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
            @Override
            public void run() {
                System.exit(0);
            }
        }, 3, TimeUnit.SECONDS);
    }
}
